package com.boot.security.service.authentication.impl;

import com.boot.cache.redis.RedisCache;
import com.boot.security.config.properties.JWTProperties;
import com.boot.security.model.vo.AccountInfo;
import com.boot.security.service.authentication.TokenService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.stereotype.Service;

import java.security.KeyPair;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

/**
 * @author 霜寒 <1621856595@qq.com>
 * @description 使用 JWT 实现 Token 操作
 * @date 2020/2/14 12:07
 **/
@EnableConfigurationProperties(JWTProperties.class)
@Service
public class JWTServiceImpl extends RedisCache<String, String> implements TokenService {

    private JWTProperties properties;
    private KeyPair keyPair;

    private ObjectMapper om;

    public static final String PREFIX = "TOKEN:";

    public JWTServiceImpl(RedisConnectionFactory redisConnectionFactory, ObjectMapper om,
                          JWTProperties properties, KeyPair keyPair) {
        super(redisConnectionFactory, om);
        this.properties = properties;
        this.keyPair = keyPair;
        this.om = om;
    }

    @Override
    public String build(AccountInfo info) {
        JwtBuilder builder = Jwts.builder()
                .setIssuer(properties.getIssuer());

        builder.signWith(keyPair.getPrivate());

        Date now = new Date();
        long expiration = info.getRememberMe() != null && info.getRememberMe() ?
                properties.getRememberTtl() : properties.getTtl();
        builder.setIssuedAt(now)
                .setNotBefore(now)
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000));

        String account;
        try {
            account = om.writeValueAsString(info);
        } catch (JsonProcessingException e) {
            throw new AuthenticationServiceException("生成签名失败：" + e.getMessage());
        }

        String jwt = builder
                .setId(String.valueOf(info.getId()))
                .setSubject(info.getUsername())
                .claim("account", account)
                .compact();

        put(PREFIX + info.getId(), jwt, expiration);

        return jwt;
    }

    @Override
    public Claims parse(String token) throws AuthenticationException {
        try {
            Claims claims = Jwts.parserBuilder()
                    .setSigningKey(keyPair.getPublic())
                    .build()
                    .parseClaimsJws(token).getBody();
            String value = get(PREFIX + claims.getId());
            if (token.equals(value)) {
                return claims;
            } else {
                throw new SessionAuthenticationException("此凭据已过期");
            }
        } catch (JwtException e) {
            throw new BadCredentialsException(String.format("凭据错误:%s", e.getMessage()));
        }
    }

    @Override
    public AccountInfo getAccountInfo(Claims claims) throws AuthenticationException {
        try {
            return om.readValue((String) claims.get("account"), AccountInfo.class);
        } catch (JsonProcessingException e) {
            throw new BadCredentialsException("验证失败：" + e.getMessage());
        }
    }

    @Override
    public void invalid(Claims claims) {
        remove(PREFIX + claims.getId());
    }

    @Override
    public void invalid(Long userId) {
        remove(PREFIX + userId);
    }

    @Override
    public boolean isNotExpired(Claims claims) throws AuthenticationException {
        return containsKey(PREFIX + claims.getId());
    }

    @Override
    public boolean isNeedUpdate(Claims claims) throws AuthenticationException {
        long expire = getExpire(PREFIX + claims.getId());
        return expire > 0 && expire <= properties.getUpdateTtl();
    }

    @Override
    public void destroy() {
        Set<String> keys = getRedisTemplate().execute((RedisCallback<Set<String>>) connection -> {
            Set<String> binaryKeys = new HashSet<>();
            Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions()
                    .match(PREFIX + '*').count(1000).build());
            while (cursor.hasNext()) {
                binaryKeys.add(new String(cursor.next()));
            }
            return binaryKeys;
        });
        remove(keys);
    }
}
